/*

FieldTool - a utility to export/import dialogues from FF7

License:
	MIT license. 

Project created on:
	project created on 2007-10-10 at 14:38

*/  

using System;
using System.IO;
using System.Text;

namespace fieldtool
{
	class MainClass
	{
		public static string version = "v0.8.4";
	
		public static string txtDir = "";
		public static bool debugMode = false;
		
		public struct fileHeader
		{
			public string name;
			public uint offset;
		}
		
		public struct dialog
		{
			public int offset;
			public byte[] text;
			public string asciiText;
		}
		
		public struct FEchar
		{
			public char chr;
			public char text;
		}
		
		public struct fileInfo
		{
			public int numEntries;
			public int dialogOffset;
			public int[] akaoOffsets;
			public int dialogCount;
		}
		
		// FF7 Table array
		public static string[] FF7Table = new string[256];
		
		// Extended chars (FE special chars)
		public static string[] FF7TableFE = new string[256];
		
		// Look-up tables for speeding up the encoding
		public static string[] FF7TableMC;
		public static byte[] FF7TableMCL;
		
		/* Export dialogues from LGP to txt files */
		
		public static void ExportLGP ( string[] arguments )
		{
			// Get arguments
			string lgpFile = arguments[1];
			txtDir = arguments[2];
			
			// Modify FF7Table a bit - add line breaks to {EOL} and {NewScreen}
			FF7Table[231] = FF7Table[231] + "\n";
			FF7Table[232] = FF7Table[232] + "\n";
			
			Console.WriteLine ( "* Opening " + lgpFile + "..." );
			
			try {
				// Open file for reading
				FileStream fs = new FileStream(lgpFile, FileMode.Open, FileAccess.Read );
				BinaryReader Reader = new BinaryReader (fs); 
				
				// Read archive signature on position 2, should be "SQUARESOFT"
				fs.Seek ( 2, 0 );
				string sig = GetString ( Reader.ReadChars ( 10 ) );
				
				if ( sig != "SQUARESOFT" )
				{
					Console.WriteLine ( "* Error: Not an LGP archive!" );
					return;
				}
				
				// Read files count
				uint fileCount = Reader.ReadUInt32();
				Console.WriteLine ( "* Success! Archive contains " + fileCount + " files." );
								
				fileHeader[] files = new fileHeader[fileCount];
				
				for ( int i = 0; i < fileCount; i++ )
				{
					files[i].name = GetString ( Reader.ReadChars(20) );
					
					// Clear out trailing 00's
					files[i].name = files[i].name.Substring ( 0, files[i].name.IndexOf ( (char) 0 ) );  
					files[i].offset = Reader.ReadUInt32();
					
					// Extracting only selected files
					for ( int j = 3; j < arguments.Length; j++ )
					{
						long oldPosition = fs.Position;
						if ( arguments[j] == files[i].name )
							extractFromFile ( files[i], fs, i );
						fs.Position = oldPosition;
					}
					
					fs.Position += 3;
				}
				
				if ( arguments.Length == 3 )
					for ( int i = 0; i < fileCount; i++ )
					{
						// Skip .tex files, .tut files, maplist and flevel.siz file
						if ( files[i].name.IndexOf ( ".tex" ) > 0 || files[i].name.IndexOf ( ".tut" ) > 0 ) continue;
						if ( files[i].name == "maplist" || files[i].name == "flevel.siz" ) continue;
						
						extractFromFile ( files[i], fs, i );
					}

				fs.Close();
			}
			
			// Exceptions 
			
			catch (System.IO.FileNotFoundException) {
				Console.WriteLine ( "Error: File not found!" );
				return;
			}
			catch (System.UnauthorizedAccessException) {
				Console.WriteLine ( "Error: Could not open lgp file!" );
				return;
			}
		}
		
		public static void extractFromFile ( fileHeader file, FileStream fs, int num )
		{
			// Unpack the file
			Console.WriteLine ( "* Unpacking " + file.name + "..." );
			MemoryStream ms = unpackFile ( file, fs, true );
			fileInfo info = readFileInfo ( file, ms );
			ms.Position = 46 + info.dialogOffset + 2; // Skip dialogue count
			
			// Read dialoues
			debug ( "Reading dialogues... " );
			dialog[] dialogues = new dialog[info.dialogCount];
			readDialogues ( ms, dialogues, file.name );
			
			// Write to file
			if ( File.Exists ( txtDir + Path.DirectorySeparatorChar + file.name + ".txt" ) )
				File.Delete ( txtDir + Path.DirectorySeparatorChar + file.name );
			FileStream ws = new FileStream(txtDir + Path.DirectorySeparatorChar + file.name + ".txt", FileMode.Create, FileAccess.Write);
			StreamWriter writer = new StreamWriter ( ws, Encoding.UTF8 );
			writer.WriteLine ( "# Level file: " + file.name + ", ID: " + num );
			writer.WriteLine ( "# Dialogue count: " + info.dialogCount );
			writer.WriteLine ( "" );					
			for ( int j = 0; j < info.dialogCount; j++ )
			{
				string spaces = "";
				if ( (j + 1) < 10 )
					spaces += "0";
				if ( (j + 1) < 100 )
					spaces += "0";
				writer.WriteLine ( "--|#" + spaces + (j + 1) + "|----------------------------------------------" );
				writer.Write ( dialogues[j].asciiText );
				if ( j < info.dialogCount - 1 )
					writer.Write ( "\n" );
			}
			writer.Flush ();
			ws.Flush ();
			ws.Close ();
		}
		
		/* Read dialog count and offsets */

		public static fileInfo readFileInfo ( fileHeader file, MemoryStream ms )
		{
			BinaryReader msReader = new BinaryReader ( ms );
		
			// Number of 'entries'
			ms.Position = 48; // 0x30 
			byte numEntries = msReader.ReadByte();

			// Dialog list offset (Section 1 header starts at 0x2A + 4 bytes - length of section)
			ms.Position = 50; // 0x32
			int dialogOffset = msReader.ReadUInt16();
			
			// Find position of AKAO pointers 
			int akaoNum = msReader.ReadUInt16();
			int[] akaoOffsets;
			if ( akaoNum > 0 )
			{
				akaoOffsets = new int[akaoNum];
				ms.Position = 78 + ( numEntries * 8 );			
				for ( int i = 0; i < akaoNum; i++ )
				{
					akaoOffsets[i] = msReader.ReadInt32() + 46; 
				}
			}
			else
			{
				// If there are no, emulate one with Section 2 offset
				// (it's used only to copy the rest of file while importing)
				akaoOffsets = new int[1];
				ms.Position = 10;
				akaoOffsets[0] = msReader.ReadInt32();
			}
			
			// Jump to dialog list and read their count
			ms.Position = 46 + dialogOffset;
			int dialogCount = msReader.ReadInt16();
			
			// Some files have no dialogs but report some random numbers
			if ( dialogCount > 1024 ||
			    file.name == "nivl_4" ||
			    file.name == "life" ||
			    file.name == "life2" )
				dialogCount = 0;
																
			// Some blin* and pillar_* files have 256 dialogues while reporting 0
			// Damn Squaresoft...
			if ( dialogCount == 0 && ( 
			    file.name.IndexOf ( "blin" ) == 0 ||
			    file.name.IndexOf ( "smkin_1" ) == 0 ||
			    file.name.IndexOf ( "pillar_" ) == 0 ) 
			)
				dialogCount = 256;
			
			fileInfo info = new fileInfo();
			info.dialogOffset = dialogOffset;
			info.akaoOffsets = akaoOffsets;
			info.dialogCount = dialogCount;
			info.numEntries = numEntries;
			return info;
		}
		
		/* Unpack file */
		
		public static MemoryStream unpackFile ( fileHeader file, FileStream fs, bool unpackPart )
		{
			BinaryReader Reader = new BinaryReader (fs);
			
			// Skip file name, read it's length and skip second length in field file
			fs.Position = file.offset + 20;
			int length = Reader.ReadInt32() - 4;
			fs.Position += 4;
								
			// Read the file
			byte[] buf = new byte[length];
			buf = Reader.ReadBytes ( length );
			
			// Decode file using LZSS					
			MemoryStream ms = new MemoryStream ( 512 * 1024 ); 
			int dstLen = LZSSDecode ( buf, ms, unpackPart );
			ms.SetLength ( dstLen );
			
			return ms;
		}
		
		/* Read dialogues from MemoryStream */
		
		public static dialog[] readDialogues ( MemoryStream ms, dialog[] dialogues, string fileName )
		{
			// Get dialog count and position of pointer table (will be added to offsets)
			int dialogCount = dialogues.Length;
			int dialogPos = (int) ms.Position - 2;
			BinaryReader msReader = new BinaryReader(ms);
			
			// Read dialog pointer table
			for ( int i = 0; i < dialogCount; i++ )
			{
				dialogues[i].offset = msReader.ReadUInt16() + dialogPos;
			}
			
			// Read dialogs
			int length = 0;
			for ( int i = 0; i < dialogCount; i++ )
			{
				// Last dialog length is calculated diffrently
				if ( i < dialogCount - 1 )
				{
					length = dialogues[i+1].offset - dialogues[i].offset;
				}
							
				// Negative length (wrong AKAO offset)
				if ( length < 0 || length > 4096 || i == dialogCount - 1 )
				{
					int startPos = (int) ms.Position;
					while ( msReader.ReadByte() != 255 ) {}
					length = (int) ms.Position - startPos;
					ms.Position = startPos;
				}
				
				dialogues[i].text = msReader.ReadBytes ( (int) length );
				dialogues[i].asciiText = textDecode ( dialogues[i].text );
			}
			
			return dialogues;
		}
		
		/* Import dialogues from txt files to LGP archive */
		
		public static void ImportLGP ( string[] arguments )
		{
			string lgpFile = arguments[1];
			txtDir = arguments[2];
			int i = 0;
			
			Console.WriteLine ( "* Opening " + lgpFile + "..." );
			
			// Load LGP file

			if ( ! File.Exists ( lgpFile ) )
			{
				Console.WriteLine ( "Error: LGP file not found!" );
				return;
			}
			
			// Open file for reading/writing
			FileStream fs = new FileStream(lgpFile, FileMode.Open, FileAccess.ReadWrite );
			BinaryReader Reader = new BinaryReader (fs); 
				
			// Read archive signature on position 2, should be "SQUARESOFT"
			fs.Seek ( 2, 0 );
			string sig = GetString ( Reader.ReadChars ( 10 ) );
			
			if ( sig != "SQUARESOFT" )
			{
				Console.WriteLine ( "* Error: Not an LGP archive!" );
				return;
			}
			
			fileHeader[] files = loadLGPTable ( fs );
			
			/* Proccess txt files */

			DirectoryInfo dir = new DirectoryInfo ( txtDir );
			FileInfo[] txtFiles = dir.GetFiles ( "*.txt" );

			
			for ( i = 0; i < txtFiles.Length; i++ )
			{
				if ( arguments.Length > 3 )
				{
					if ( Array.IndexOf ( arguments, txtFiles[i].Name.Substring ( 0, txtFiles[i].Name.IndexOf ( "." ) ) ) == -1 ) 
						continue;
				}

				Console.WriteLine ( "* Processing " + txtFiles[i].Name + "..." );
				
				// Open txt file
				debug ( "Opening txt file..." );
				FileStream txtfs = new FileStream ( txtDir + Path.DirectorySeparatorChar + txtFiles[i].Name, FileMode.Open, FileAccess.Read );
				StreamReader reader = new StreamReader ( txtfs, Encoding.UTF8 );
				string line = null;

				// Read dialogue ID and count
				debug ( "Reading File ID and count..." );
				line = reader.ReadLine();
				int dialogID = Convert.ToInt32 ( line.Substring ( line.IndexOf ( "," ) + 5 ) );
				line = reader.ReadLine();
				int dialogCount = Convert.ToInt32 ( line.Substring ( 18 ) );
				debug ( "File ID: " + dialogID + ", Dialog count: " + dialogCount );
				
				// Skip next two lines
				reader.ReadLine();
				reader.ReadLine();
				
				int idx = 0;
				dialog[] dialogues = new dialog[dialogCount];
				
				// Read the dialogues
				debug ( "Reading dialogues..." );
				while ( ( line = reader.ReadLine() ) != null )
				{
					if ( line.IndexOf ( "--|" ) == 0 )
						idx++;
					else
						dialogues[idx].asciiText += line;
				}
				
				txtfs.Close();
				
				// Encode dialogues with FF7Text
				debug ( "Encoding dialogues..." );
				DateTime startTime = DateTime.Now;
			
				for ( idx = 0; idx < dialogCount; idx++ )
				{
					dialogues[idx].text = textEncode ( dialogues[idx].asciiText ); 
				}
				
				DateTime stopTime = DateTime.Now;
				TimeSpan elapsed = stopTime - startTime;
				
				debug ( "Text encoding done in: " + elapsed.TotalMilliseconds + " ms" );
	
				// Get file information
				debug ( "Unpacking " + files[dialogID].name + " from LGP..." );
				MemoryStream ms = unpackFile ( files[dialogID], fs, false );
				fileInfo info = readFileInfo ( files[dialogID], ms );
				BinaryReader msReader = new BinaryReader ( ms );
				
				// Read other sections past and before dialogues
				debug ( "Going to first AKAO offset: 0x" + Convert.ToString(info.akaoOffsets[0], 16) + ", reading rest of file: " + ( (int) ms.Length - info.akaoOffsets[0] ) + " bytes..." );
				ms.Position = info.akaoOffsets[0];				
				byte[] pastDialogues = new byte [ms.Length - info.akaoOffsets[0]];
				ms.Read ( pastDialogues, 0, (int) ms.Length - info.akaoOffsets[0] );
				
				// Calculate end of dialogues section
				// This is not necessairly the AKAO pointer offset
				// Some level files have this messed up, so this is safer
				ms.Position = 46 + info.dialogOffset + ( dialogues.Length * 2 );
				long lastOffset = msReader.ReadInt16() + 46 + info.dialogOffset;
				debug ( "Calculating end of dialogues section..." );
				ms.Position = lastOffset;
				while ( msReader.ReadByte() != 255 ) {}
				int dialogueEnd = (int) ms.Position;
				
				// Go to dialogues pointer table
				ms.Position = 46 + info.dialogOffset;
				int dialogueStart = (int) ms.Position;
				BinaryWriter msWriter = new BinaryWriter ( ms );
				
				// Skip dialogue count
				ms.Position += 2;

				// Write pointer table
				debug ( "Rewriting pointer table..." );
				short currentPos = (short) (dialogues.Length * 2 + 2) ;
				for ( idx = 0; idx < dialogues.Length; idx++ )
				{
					msWriter.Write ( currentPos );
					currentPos += (short) dialogues[idx].text.Length;
				}
				
				// Write dialogues
				debug ( "Rewriting dialogues..." );
				for ( idx = 0; idx < dialogues.Length; idx++ )
				{
					msWriter.Write ( dialogues[idx].text );
				}
				
				int diff = (int) ms.Position - dialogueEnd;
				
				// 00 Padding to AKAO section
				debug ( "Writing 0x00 padding to AKAO section: " + (info.akaoOffsets[0] - dialogueEnd) + " bytes..." );
				for ( int j = 0; j < info.akaoOffsets[0] - dialogueEnd; j++ )
				{
					msWriter.Write ( (byte) 0 );
				}
				
				// Write remaining sections (TODO: Only if size changed)
				debug ( "Writing rest of the file..." );
				msWriter.Write ( pastDialogues );
				int fileEnd = (int) ms.Position;
				
				// Rewrite section and akao pointers
				debug ( "Rewriting section and AKAO pointers..." );
				if ( diff != 0 )
				{
					// Section pointers
					ms.Position = 10;
					for ( idx = 0; idx < 8; idx++ )
					{
						int pointer = msReader.ReadInt32();
						ms.Position = ms.Position - 4;
						msWriter.Write ( (int) (pointer + diff) );
					}
					
					// AKAO pointers
					ms.Position = 52;
					byte akaoNum = msReader.ReadByte();
					ms.Position = 78 + ( info.numEntries * 8 );
					for ( idx = 0; idx < akaoNum; idx++ )
					{
						msWriter.Write ( info.akaoOffsets[idx] - 46 + diff );
					} 
				}
				
				// Ok, done, let's write it to file
				byte[] buf = new byte[fileEnd];
				ms.Position = 0;
				ms.Read ( buf, 0, fileEnd );
				
				// Write it to the end of LGP
				debug ( "Writing all to the end of LGP..." );
				FileInfo lgpInfo = new FileInfo ( lgpFile );
				fs.Position = lgpInfo.Length - 14; // Erase old "FINAL FANTASY7" mark
				int newOffset = (int) fs.Position;
				BinaryWriter Writer = new BinaryWriter ( fs );
				
				// Write file name and it's 00 padding
				Writer.Write ( files[dialogID].name.ToCharArray() );
				for ( int j = 0; j < 20 - files[dialogID].name.Length; j++ )
					Writer.Write ( (byte) 0 );
				
				// Write nulls for file size temporarily
				Writer.Write ( (int) 0 );

				// Encode and write field file
				int fileLength = LZSSEncode ( buf, fs );
//				FileStream lvlfs = new FileStream ( "test", FileMode.Create, FileAccess.ReadWrite );
//				int fileLength = LZSSEncode ( buf, lvlfs );
//				lvlfs.Close();
//				return;
				
				// Write end of LGP mark
				Writer.Write ( "FINAL FANTASY7".ToCharArray() );

				// Write file size
				fs.Position = newOffset + 20;
				Writer.Write ( (int) fileLength + 4 );
				
				// Update LGP Index
				debug ( "Updating LGP index, new offset: 0x" + Convert.ToString(newOffset, 16) + "..." );
				fs.Position = 16 + (27 * dialogID) + 20;
				Writer.Write ( newOffset );
				
				// Close LGP file to save changes, and re-open if for next file
				fs.Flush();
				fs.Close();
				fs = new FileStream(lgpFile, FileMode.Open, FileAccess.ReadWrite );
				Reader = new BinaryReader (fs); 
			}
			fs.Close();
		}
		
		/* Fast copy routine */
		
		public static byte[] fastCopy ( byte[] from, int len )
		{
			int s = 0; int d = 0;
			byte[] output = new byte[len];
			while ( s != len )
				output[d++] = from[s++];
			return output;
		}
		
		/* Check if array contains the string */
		
		public static int indexInFF7Table ( string txt )
		{
			int tblLen = FF7TableMC.Length;
			for ( int i = 0; i < tblLen; i++ )
			{ 
				if ( FF7TableMC[i].IndexOf ( txt ) == 0 )
					return i;
			}
			return -1;
		}
		
		/* Check if string could be multichar */
		
		public static bool isInMCL ( byte chr )
		{
			int mcl_pos = 0;
			while ( FF7TableMCL[mcl_pos] != 255 )
			{
				if ( FF7TableMCL[mcl_pos++] == chr ) return true;
			}
			return false;
		}
		
		/* FF7 text encoding */
		
		public static byte[] textEncode ( string text )
		{			
			byte[] tmp = new byte[4096];
			int src_pos = 0;
			int dst_pos = 0;
			int len = text.Length;
						
			while ( src_pos != len )
			{
				int iLen = 1;
				// Check for multicharacter entities
				if ( isInMCL ( (byte) text[src_pos] ) )
				{
					while ( src_pos + iLen < len && indexInFF7Table ( text.Substring ( src_pos, iLen + 1 ) ) != -1 )
					{
						iLen++;
					}
					
					// If nothing matched, reset entity length to 1
					if ( Array.IndexOf ( FF7TableFE, text.Substring ( src_pos, iLen ) ) == -1 &&
					     Array.IndexOf ( FF7Table, text.Substring ( src_pos, iLen ) ) == -1 )
					     iLen = 1;
				}
				
				// Pick either from normal table on special character table
				if ( dst_pos > 0 && tmp[dst_pos - 1] == 254 )
					tmp[dst_pos++] = (byte) Array.IndexOf ( FF7TableFE, text.Substring ( src_pos, iLen ) );
				else
					tmp[dst_pos++] = (byte) Array.IndexOf ( FF7Table, text.Substring ( src_pos, iLen ) );
				
				src_pos += iLen;
			}
			byte[] output = fastCopy ( tmp, dst_pos );
			
			return output;
		}
		
		/* FF7 text decoding */
		
		public static string textDecode ( byte[] text )
		{
			string output = "";
			for ( int i = 0; i < text.Length; i++ )
			{
				// Special: FE
				if ( i > 0 && text[i-1] == 254 )
					output += FF7TableFE[text[i]];
				
				// Ordinary chars
				else			
					output += FF7Table[text[i]];
			}
			return output;
		}
		
		/* Load LGP Table */
		
		public static fileHeader[] loadLGPTable ( FileStream fs ) 
		{
			/** LGP Table {
			  * - name: file name, string, padded with 00's (20 bytes)
			  * - offset: file offset, int (4 bytes)
			  * - unknown: 3 bytes
			  */

			BinaryReader Reader = new BinaryReader ( fs );
			
			// Read files count
			uint fileCount = Reader.ReadUInt32();
			Console.WriteLine ( "* Success! Archive contains " + fileCount + " files." );
							
			fileHeader[] files = new fileHeader[fileCount];
			
			for ( int i = 0; i < fileCount; i++ )
			{
				files[i].name = GetString ( Reader.ReadChars(20) );
				
				// Clear out trailing 00's
				files[i].name = files[i].name.Substring ( 0, files[i].name.IndexOf ( (char) 0 ) );  
				files[i].offset = Reader.ReadUInt32();
				
				fs.Position += 3;
			}
			
			return files;
		}
		
		/* LZSS Encode function, credits to Qhimm, altered for C# */
				
		public static int LZSSEncode ( byte[] src, FileStream fs )
		{
			// Initialize variables
			int controlbits = 0; int control = 0; int p_control = 4;
			int p_src = 0; int dst_start = (int) fs.Position;
			int src_len = src.Length;
			int p_src_max1 = src_len - 16; int p_src_max16 = src_len - 128;
			int[] buffer = new int[256*256*256]; // 16 MB buffer
			
			// Leave space for file size and first control byte
			fs.Position += 5;
			
			while ( true )
			{
				// Initialize some more variables
				int p = 0; int s = 0;
				int unroll = 8;	int len = 0;
				int index = 0; long offset = 0;
				
				// Last section
				if ( p_src > p_src_max16 )
				{
					unroll = 1;
					if ( p_src > p_src_max1 )
					{
						if ( p_src == src_len ) break;
						goto literal;
					}
				}
		begin_unrolled_loop:
					
				// Calculate index for buffer (that's a good one, Qhimm!)
				//index = ((40543 * ((((src[p_src] << 4) ^ src[p_src + 1]) << 4) ^ src[p_src + 2])) >> 4) & 0xFFF;
					
				// My own index calculation for 16MB buffer, cut's off a few kB's from output file
				index = (((( src[p_src] << 16 ) ) ^ ( src[p_src + 1] << 8 )) ^ ( src[p_src + 2] ));
//				Console.WriteLine ( "First chars: " + src[p_src] + " " + src[p_src + 1] + " " + src[p_src + 2] + " , Index: " + index + ", Idx value: " + buffer[index] );
				p = buffer[index];
//				if ( index == 0 ) p = 0;
				buffer[index] = s = p_src;
				offset = s - p;
		literal:
				if (( offset >= 4096 || p < 0 || offset == 0 || p_src > p_src_max1 ||
				    src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++])) {
					
					// Write nulls
					// Don't know how to handle them properly
					/*
					if ( src[p_src] == 0 && src[p_src + 1] == 0 && src[p_src + 2] == 0 ) 
					{
						s = p_src;
						while ( src[s++] == 0 ) {}
						len = s - p_src - 1;
						
						if ( len > 16 ) len = 16;
						
						for ( int i = len - 2; i < len; i++ )
						{
							index = (((( src[p_src + i] << 16 ) ) ^ ( src[p_src + 1 + i] << 8 )) ^ ( src[p_src + 2 + i] ));
							buffer[index] = p_src + i;
						}

								
						// Write negative offset
						offset = 0xFED - ( len - 3 );
						fs.WriteByte ( (byte) offset );
						fs.WriteByte ( (byte) ((( offset & 0xF00 ) >> 4 ) + ( len - 3 )));

						// Control bits
						p_src += len;
						control = ( control >> 1 ) | 0x80;
						controlbits++;
					}
					*/
						
					// Write literal if offset is out of buffer, is at the beginning of the file, etc.
//					else {
						fs.WriteByte ( src[p_src++] );
						control >>= 1;
						controlbits++;
//					}
				}
				else {
					// Finding matching bytes and their length
					if ( src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] ||
					src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] ||
					src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] || src[p++] != src[s++] ||
					src[p++] != src[s++] ) { }
					len = s - p_src - 1;
					
					// Calculate indexes for each substring of reference
					for ( int i = 1; i < len; i++ )
					{
						index = (((( src[p_src + i] << 16 ) ) ^ ( src[p_src + 1 + i] << 8 )) ^ ( src[p_src + 2 + i] ));
						buffer[index] = p_src + i;
					}
					
					// Calculate offset
					offset = (p_src - offset - 18) & 0xFFF;
					fs.WriteByte ( (byte) offset );
					fs.WriteByte ( (byte) ((( offset & 0xF00 ) >> 4 ) + ( len - 3 )));
					
					// Control bits
					p_src += len;
					control = ( control >> 1 ) | 0x80;
					controlbits++;
				}
				
				if ( --unroll > 0 ) goto begin_unrolled_loop;
				
				if ( controlbits == 8 )
				{
					// Write control byte
					long oldPos = fs.Position;
					fs.Position = dst_start + p_control;
					fs.WriteByte ( (byte) (control ^ 0xFF) );
					fs.Position = oldPos;
					
					p_control = (int) fs.Position - dst_start;
					control = controlbits = 0;
					if ( p_src != src_len ) fs.Position++;
				}
//				if ( p_src > 256 ) return 1; // Stopper
			}
			
			control >>= 8 - controlbits;

			// Write file length
			long lastPos = fs.Position;
			int fileLength = (int) (lastPos - dst_start - 4);
			fs.Position = dst_start;
			BinaryWriter w = new BinaryWriter ( fs );
			w.Write ( fileLength );
			
			// Write last control byte
			if ( controlbits > 0 )
			{
				fs.Position = dst_start + p_control;
				fs.WriteByte ( (byte) (control ^ 0xFF) );
			}
			
			fs.Position = lastPos;

			return fileLength;
		}
				
		/* LZSS Decode function, by Qhimm, altered to read only field script & dialog section */
		
		public static int LZSSDecode ( byte[] src, MemoryStream ms, bool readPart )
		{
			// Initialize variables
			int controlbits = 0;
			int control = 0;
			int p_src = 0;
			int p_dst = 0;
			int src_len = src.Length;
			int p = 0;
			int sectionEnd = 0;
			byte[] sectionEndArr = new byte[4];
			byte[] dst = new byte[1024 * 1024];
			
			// Read until end of file or section
			while (p_src != src_len && ( sectionEnd == 0 || p_dst <= (sectionEnd + 1024) || ! readPart )) {
				
				// Read control byte
				if (controlbits == 0) {
					control = src[p_src++];
					controlbits = 8;
				}
				
				// Reference
				if ( (control & 1) == 0 ) {
					int offset, len; 
					offset = src[p_src++] & 0xFF;
					offset += (src[p_src] & 0xF0) << 4;
					len = 3 + (src[p_src++] & 0xF);
					p = p_dst - ((p_dst - (offset + 18)) & 0xFFF);

					while (len-- > 0) {
						
						// Negative offset
						if (p < 0) {
							dst[p_dst++] = 0;
							p++;
						}
						else
							dst[p_dst++] = dst[p++];
					}
				}
				
				// Literal
				else
					dst[p_dst++] = src[p_src++];
				
				control >>= 1;
				controlbits--;
				
				// Reading only the first section
				if ( p_dst >= 14 && sectionEnd == 0 )
				{
					sectionEndArr[0] = dst[10];
					sectionEndArr[1] = dst[11];
					sectionEndArr[2] = dst[12];
					sectionEndArr[3] = dst[13];
					sectionEnd = BitConverter.ToInt32 ( sectionEndArr, 0 ) + 4;
				}
			}
			ms.Write ( dst, 0, dst.Length );
			return (int) p_dst;
		}
		
		public static void loadFF7Table () 
		{
			FileStream fs = new FileStream ( "fieldtool.tbl", FileMode.Open, FileAccess.Read );
			StreamReader reader = new StreamReader ( fs, Encoding.UTF8 );
			int FF7TableLength = FF7Table.Length;
			string line = null;
			int mc = 0;

			// Load file line by line, first 256 lines are normal characters
			for ( int i = 0; i < FF7TableLength; i++ )
			{
				line = reader.ReadLine();
				FF7Table[i] = line.Substring ( 3 );
				if ( FF7Table[i].Length > 1 )
					mc++;
			}

			// Skip comment line
			reader.ReadLine();
						
			// Load extended chars
			while ( ( line = reader.ReadLine() ) != null )
			{
				FF7TableFE[Convert.ToInt32(line.Substring(0, 2), 16)] = line.Substring(3);
				mc++;
			}

			// Build look-up tables
			// FF7TableMC - contains multichar entities
			// FF7TableMCL - Look-up table for the one above, contains first chars of MC entities
			FF7TableMC = new string[mc];
			FF7TableMCL = new byte[mc];
			
			int idx = 0;
			int idxMcl = 0;
			for ( int i = 0; i < FF7TableLength; i++ )
			{
				if ( FF7Table[i].Length > 1 )
				{
					FF7TableMC[idx] = FF7Table[i];
					idx++;
					if ( Array.IndexOf ( FF7TableMCL, (byte) FF7Table[i][0] ) == -1 )
					{
						FF7TableMCL[idxMcl] = (byte) FF7Table[i][0];
						idxMcl++;
					}
				}
			}
			for ( int i = 0; i < FF7TableLength; i++ )
			{
				if ( FF7TableFE[i] != null )
				{
					FF7TableMC[idx] = FF7TableFE[i];
					idx++;
					if ( Array.IndexOf ( FF7TableMCL, (byte) FF7TableFE[i][0] ) == -1 )
					{
						FF7TableMCL[idxMcl] = (byte) FF7TableFE[i][0];
						idxMcl++;
					}
				}
			}
			// Write terminator to MCL table
			FF7TableMCL[idxMcl] = 255;
		}
		
		// Custom Char to String conversion
		
		public static string GetString ( char[] chars )
		{
			return new String ( chars );
		}
		
		// Debug function
		
		public static void debug ( string message )
		{
			if ( debugMode )
				Console.WriteLine ( "* DEBUG: " + message );
		}
		
		/* Command-line handling procedures */
		
		public static void Header ()
		{
			Console.WriteLine ( "FF7 FieldTool " + version + " by mav" );
			Console.WriteLine ( "-------------------------" );
			Console.WriteLine ( "" );
		}
		public static void Help ()
		{
			Console.WriteLine ( "Usage: fieldtool.exe <MODE> <LGP-FILE> <TXT-DIR> [FILE(s)]" );
			Console.WriteLine ( "" );
			Console.WriteLine ( "MODE - one of following:" );
			Console.WriteLine ( "       e - export dialogues from archive" );
			Console.WriteLine ( "       i - import dialogues to archive" );
			Console.WriteLine ( "LGP-FILE - lgp file to work on" );
			Console.WriteLine ( "TXT-DIR - output/input directory" );
			Console.WriteLine ( "FILE(s) (optional) - work only on these field files (separated by space)" );
		}
		public static void Main(string[] args)
		{
			Header();
			
			if ( args.Length != 0 && args[0].Length == 2 && args[0][1] == 'd' )
				debugMode = true;
			
			if ( args.Length == 0 )
			{
				Console.WriteLine ( "Error: No parameters given!" );
				Console.WriteLine ( "" );
				Help();
			}
			else if ( args.Length < 3 )
			{
				Console.WriteLine ( "Error: Not enough parameters!" );
				Console.WriteLine ( "" );
			}
			else if ( ! File.Exists ( "fieldtool.tbl" ) )
			{
				Console.WriteLine ( "Error: FF7Table file 'fieldtool.tbl' not found!" );
				Console.WriteLine ( "" );
			}
			else if ( args[0][0] == 'e' )
			{
				loadFF7Table();
				ExportLGP(args);
			}
			else if ( args[0][0] == 'i' )
			{
				loadFF7Table();
				ImportLGP(args);
			}
			else
			{
				Console.WriteLine ( "Error: Wrong parameter!" );
				Console.WriteLine ( "" );
				Help();
			}
		}
	}
}